마이페이지 기능 구현 및 디자인 개선#690
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
백엔드 응답에 없는 saved 필드 제거 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
primary-500 미존재 토큰을 fill-brand-default-default 등으로 교체, GiftEmailCard 제거, disabled 버튼 스타일 수정 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
디스코드/피드 배너 레이아웃 개선, primary-500 미존재 토큰을 text-text-brand/border-border-brand으로 교체, 사이드바 라벨 변경 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
bg/text/border-primary-500, bg-brand-primary-500을 프로젝트 토큰 (fill-brand-default-default, text-brand, border-brand, rose-500 등)으로 일괄 교체 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
discord-icon.png, feed-icon.svg 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Your plan includes 1 review of capacity. Refill in 11 minutes and 28 seconds. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more review capacity refills, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthrough알림 모달·로컬 비활성화 도입, 1:1 문의 훅·스키마 추가, 마이페이지 프로필·네비 및 모달 스타일 변경, 빌더 피드 필터·드래프트·삭제 기능, 결제 환불/가상계좌 흐름과 axios v6 클라이언트 추가를 통합합니다. Changes학습 알림 설정 및 모달 기능 확장
1:1 문의 시스템 구현
마이페이지 프로필 및 네비게이션 개선
빌더 피드 관리 및 드래프트
결제·환불 흐름 및 API v6 도입
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/app/(service)/(my)/my-inquiry/write/page.tsx (1)
50-53:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift첨부 이미지가 실제 요청에는 한 번도 포함되지 않습니다.
사용자가 파일을 선택해도 로컬 상태에만 쌓이고, 임시저장 요청에는 첨부 정보가 없으며 등록 요청은 항상
inquiryAttachmentKeys: []를 보냅니다. 지금 UI는 첨부가 저장된 것처럼 보이는데 실제로는 전부 버려집니다. 업로드/키 발급 플로우를 연결하기 전이라면 첨부 UI를 숨기거나 비활성화하는 편이 안전합니다.Also applies to: 62-68, 86-92
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`(service)/(my)/my-inquiry/write/page.tsx around lines 50 - 53, The image input currently only updates local state via handleImageChange and setImages and never uploads files or attaches generated keys to requests, so temp-save/submit always send inquiryAttachmentKeys: []; either wire the upload/key-issuance flow so selected files are uploaded and their attachment keys are stored and included in the payloads used by the tempSave and submit handlers, or disable/hide the attachment UI until that flow exists; specifically update handleImageChange and the images state to call your upload function that returns keys (or map files→keys), store those keys (e.g., attachmentKeys state), and ensure the temp-save/submit routines reference attachmentKeys instead of always sending an empty array (or conditionally render/disable the file input element until implemented).
🧹 Nitpick comments (1)
src/app/(service)/(my)/my-posts/page.tsx (1)
249-256: ⚡ Quick winAPI 기반 이미지 URL은
<Image>렌더 전에 허용 도메인 검증이 필요합니다.
feed.thumbnailUrl을 바로<Image src>에 넣고 있어,next.config허용 도메인 밖 URL이면 런타임에서 깨질 수 있습니다. 공통 URL 가드 유틸로 선검증 후, 실패 시 플레이스홀더만 렌더링하는 방식으로 바꿔 주세요.Based on learnings API-sourced URLs in this repository should be validated against
next.config.tsremotePatternsbefore rendering<Image>, instead of relying ononError.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`(service)/(my)/my-posts/page.tsx around lines 249 - 256, Validate feed.thumbnailUrl against the app's allowed remote image patterns before passing it to the Next.js <Image> component: add or use a common URL guard (e.g., isAllowedRemoteImage or validateRemoteImageUrl) to check feed.thumbnailUrl; only render <Image src={feed.thumbnailUrl} ...> when the guard returns true and imgError is false, otherwise render the placeholder fallback; keep the onError handler to flip setImgError(true) but do not rely on it as the primary validation and ensure the guard uses the same remotePatterns defined in next.config (or a central export) so allowed domains stay in sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/`(service)/(my)/class-payment-management/page.tsx:
- Line 239: Replace the hardcoded Tailwind color utilities "from-rose-500
to-rose-300" used in the div with className "h-1000 w-1000 flex-shrink-0
rounded-150 bg-gradient-to-br from-rose-500 to-rose-300" by the project's theme
gradient token defined in global.css (e.g., a bg-fill-brand-* or the appropriate
gradient token); edit that className to remove the literal color utilities and
apply the single theme token class so the component relies on `@theme` inline
tokens rather than hardcoded Tailwind colors.
In `@src/app/`(service)/(my)/my-class/page.tsx:
- Line 204: The conditional divider uses a raw Tailwind color ('bg-rose-200')
which violates the project's styling tokens rule; in the expression
className={cn('h-px', isEnabled ? 'bg-rose-200' : 'bg-border-default')}, replace
'bg-rose-200' with the appropriate project custom token class defined in
global.css (the same token used elsewhere for active/primary dividers in
src/app, e.g., the project's "active divider" or "accent" token) so both
branches use project tokens instead of raw Tailwind colors.
- Around line 42-46: The component reads localStorage during initial render
which causes SSR/CSR mismatch; change the useState initializer for
locallyDisabled to a constant default (e.g., false) and add a useEffect that, on
mount, reads localStorage.getItem(NOTIFICATION_DISABLE_KEY) and calls
setLocallyDisabled accordingly (use the existing locallyDisabled and
setLocallyDisabled identifiers). Also replace hardcoded Tailwind color classes
(e.g., bg-rose-200, bg-gray-200) used in this file with semantic theme tokens
defined in global.css (create/consume classes like bg-accent-weak or bg-muted
that map to your CSS variables) so the component uses the theme tokens instead
of direct color utilities.
In `@src/app/`(service)/(my)/my-inquiry/page.tsx:
- Around line 39-40: The page currently treats a failed fetch as empty because
it only reads `inquiries` from useGetMyOneToOneInquiries(); update the component
to detect and handle the query error instead of rendering the “no inquiries”
empty state: use the error information returned by the hook (or catch the
rejection) and pass it to the centralized error handler in
utils/error-handler.ts (which will use useToastStore) rather than calling alert
or silently rendering an empty list; ensure both the initial query
(useGetMyOneToOneInquiries) and the related code at the other occurrence (lines
~69-73) call the same handler and render an error UI or loading state until data
is valid.
- Around line 95-96: The detail query call using useGetMyOneToOneInquiryDetail
(with expanded ? inquiry.oneToOneInquiryId : null) currently only handles
loading/success and leaves the panel empty on failure; update the component to
read the hook's error state (e.g., isError and error) alongside detail and
detailLoading, call the centralized error handler from utils/error-handler.ts
(and/or useToastStore) to report the error, and render an explicit
error/fallback UI in the panel (a brief error message or retry button) when
isError is true so the user doesn’t see an empty panel; apply the same pattern
to the related rendering block around the 139-173 area.
In `@src/app/`(service)/(my)/my-posts/page.tsx:
- Line 248: Replace the forbidden Tailwind arbitrary class "aspect-[3/2]" on the
<div className="relative aspect-[3/2] w-full bg-gray-100"> JSX element in
page.tsx with the project’s predefined aspect-ratio token/utility (e.g., the
project's aspect token such as aspect-<token> or ratio-<token>); locate that JSX
node (in the default export component of page.tsx) and swap the arbitrary value
for the correct token/utility class used across the codebase.
- Around line 110-131: The three filter buttons in my-posts page.tsx (the
buttons with visible labels "코스", "레슨", and "최신순") are purely presentational and
need either real handlers or a disabled visual state; fix by wiring them to
state and handlers (e.g., add a useState like selectedFilter/setSelectedFilter
and a handleFilterClick that toggles "코스" vs "레슨", and a
selectedSort/setSelectedSort + handleSortClick for "최신순", then pass
onClick={handleFilterClick("코스")} etc. and update rendering based on selected
values), or if the feature isn’t ready, add disabled and aria-disabled
attributes and apply a dimmed class (e.g., opacity-50, cursor-not-allowed) to
each button so they are visually and semantically non-interactive; ensure
accessibility attributes (aria-pressed or aria-disabled) reflect the chosen
approach.
- Around line 78-79: Replace hardcoded Tailwind color classes in the conditional
className (the ternary that returns 'border-b-2 border-rose-500 text-rose-500'
vs 'font-designer-20r text-gray-800') and the other occurrences mentioned (the
lines using 'border-rose-300' and 'hover:text-red-500') with the project’s
design token classes defined in global.css; locate the conditional that sets
those class strings in src/app/(service)/(my)/my-posts/page.tsx and swap each
color class (border-rose-500, text-rose-500, border-rose-300,
hover:text-red-500, text-gray-800 as appropriate) for the equivalent token-based
classes (the token names used elsewhere in global.css) so styling follows the
token convention.
In `@src/types/schemas/inquiry.schema.ts`:
- Around line 26-29: The inquiryContent string schema currently uses .min(1)
which allows strings containing only whitespace; update the schema for
inquiryContent to reject whitespace-only input by trimming or validating
whitespace before length check—for example, apply .transform(s => s.trim()) then
.min(1, '내용을 입력해 주세요.') or use .refine(s => s.trim().length > 0, { message: '내용을
입력해 주세요.' }) so the schema behavior matches the form's trim logic.
---
Outside diff comments:
In `@src/app/`(service)/(my)/my-inquiry/write/page.tsx:
- Around line 50-53: The image input currently only updates local state via
handleImageChange and setImages and never uploads files or attaches generated
keys to requests, so temp-save/submit always send inquiryAttachmentKeys: [];
either wire the upload/key-issuance flow so selected files are uploaded and
their attachment keys are stored and included in the payloads used by the
tempSave and submit handlers, or disable/hide the attachment UI until that flow
exists; specifically update handleImageChange and the images state to call your
upload function that returns keys (or map files→keys), store those keys (e.g.,
attachmentKeys state), and ensure the temp-save/submit routines reference
attachmentKeys instead of always sending an empty array (or conditionally
render/disable the file input element until implemented).
---
Nitpick comments:
In `@src/app/`(service)/(my)/my-posts/page.tsx:
- Around line 249-256: Validate feed.thumbnailUrl against the app's allowed
remote image patterns before passing it to the Next.js <Image> component: add or
use a common URL guard (e.g., isAllowedRemoteImage or validateRemoteImageUrl) to
check feed.thumbnailUrl; only render <Image src={feed.thumbnailUrl} ...> when
the guard returns true and imgError is false, otherwise render the placeholder
fallback; keep the onError handler to flip setImgError(true) but do not rely on
it as the primary validation and ensure the guard uses the same remotePatterns
defined in next.config (or a central export) so allowed domains stay in sync.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: dfa985e9-5ce0-4f98-9d57-46f3813e886f
⛔ Files ignored due to path filters (2)
public/my-page/discord-icon.pngis excluded by!**/*.pngpublic/my-page/feed-icon.svgis excluded by!**/*.svg
📒 Files selected for processing (17)
src/api/client/axios.tssrc/app/(service)/(my)/class-payment-management/page.tsxsrc/app/(service)/(my)/my-class/_components/disable-notification-modal.tsxsrc/app/(service)/(my)/my-class/_components/learning-notification-modal.tsxsrc/app/(service)/(my)/my-class/page.tsxsrc/app/(service)/(my)/my-inquiry/page.tsxsrc/app/(service)/(my)/my-inquiry/write/page.tsxsrc/app/(service)/(my)/my-page/_components/withdrawal-confirm-modal.tsxsrc/app/(service)/(my)/my-page/page.tsxsrc/app/(service)/(my)/my-posts/page.tsxsrc/app/(service)/(my)/payment-management/page.tsxsrc/components/common/layout/sidebar/my-page-mobile-nav.tsxsrc/components/common/layout/sidebar/my-page-sidebar.tsxsrc/components/common/modals/user-profile-modal.tsxsrc/hooks/queries/my-inquiry/inquiry-api.tssrc/hooks/queries/notification/use-notification-setting.tssrc/types/schemas/inquiry.schema.ts
💤 Files with no reviewable changes (1)
- src/hooks/queries/notification/use-notification-setting.ts
| <div className="border-border-subtle rounded-200 flex gap-300 border p-300"> | ||
| {/* 썸네일 */} | ||
| <div className="h-1000 w-1000 flex-shrink-0 rounded-150 bg-gradient-to-br from-primary-500 to-rose-300" /> | ||
| <div className="h-1000 w-1000 flex-shrink-0 rounded-150 bg-gradient-to-br from-rose-500 to-rose-300" /> |
There was a problem hiding this comment.
그라데이션 색상에 하드코딩된 Tailwind 유틸리티 사용
from-rose-500 to-rose-300은 표준 Tailwind 색상 유틸리티이며, 프로젝트의 global.css에 정의된 커스텀 토큰이 아닙니다. 코딩 가이드라인에 따르면, src/app/**/*.{tsx,jsx} 파일에서는 하드코딩된 색상을 사용하지 않고 global.css의 @theme inline 토큰만 사용해야 합니다.
bg-fill-brand-* 또는 프로젝트에 정의된 적절한 그라데이션 토큰으로 교체하시기 바랍니다.
As per coding guidelines: src/app/**/*.{tsx,jsx}: No hardcoded colors or spacing values. Use only @theme inline tokens from global.css.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/`(service)/(my)/class-payment-management/page.tsx at line 239,
Replace the hardcoded Tailwind color utilities "from-rose-500 to-rose-300" used
in the div with className "h-1000 w-1000 flex-shrink-0 rounded-150
bg-gradient-to-br from-rose-500 to-rose-300" by the project's theme gradient
token defined in global.css (e.g., a bg-fill-brand-* or the appropriate gradient
token); edit that className to remove the literal color utilities and apply the
single theme token class so the component relies on `@theme` inline tokens rather
than hardcoded Tailwind colors.
| const { data: inquiries, isLoading } = useGetMyOneToOneInquiries(); | ||
|
|
There was a problem hiding this comment.
목록 조회 실패를 빈 상태로 숨기지 마세요.
지금은 목록 쿼리가 실패해도 inquiries가 undefined라서 곧바로 “아직 작성한 문의가 없어요”가 렌더링됩니다. 서버 오류를 빈 데이터로 오인하게 됩니다.
🔧 제안
- const { data: inquiries, isLoading } = useGetMyOneToOneInquiries();
+ const {
+ data: inquiries,
+ isLoading,
+ isError,
+ } = useGetMyOneToOneInquiries();
...
- ) : !inquiries || inquiries.length === 0 ? (
+ ) : isError ? (
+ <div className="flex items-center justify-center py-600">
+ <p className="font-designer-14r text-text-subtle">
+ 문의 목록을 불러오지 못했어요. 잠시 후 다시 시도해 주세요.
+ </p>
+ </div>
+ ) : inquiries.length === 0 ? (As per coding guidelines: "All errors must be handled via utils/error-handler.ts — never use alert(), always use useToastStore."
Also applies to: 69-73
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/`(service)/(my)/my-inquiry/page.tsx around lines 39 - 40, The page
currently treats a failed fetch as empty because it only reads `inquiries` from
useGetMyOneToOneInquiries(); update the component to detect and handle the query
error instead of rendering the “no inquiries” empty state: use the error
information returned by the hook (or catch the rejection) and pass it to the
centralized error handler in utils/error-handler.ts (which will use
useToastStore) rather than calling alert or silently rendering an empty list;
ensure both the initial query (useGetMyOneToOneInquiries) and the related code
at the other occurrence (lines ~69-73) call the same handler and render an error
UI or loading state until data is valid.
| const { data: detail, isLoading: detailLoading } = | ||
| useGetMyOneToOneInquiryDetail(expanded ? inquiry.oneToOneInquiryId : null); |
There was a problem hiding this comment.
상세 조회 실패 시 패널이 비어 보입니다.
상세 쿼리가 실패하면 현재 분기에서는 아무 내용도 렌더링되지 않아서, 사용자가 빈 문의로 오해할 수 있습니다. 로딩/성공 외에 실패 상태도 명시적으로 처리해 주세요.
🔧 제안
- const { data: detail, isLoading: detailLoading } =
+ const {
+ data: detail,
+ isLoading: detailLoading,
+ isError: isDetailError,
+ } =
useGetMyOneToOneInquiryDetail(expanded ? inquiry.oneToOneInquiryId : null);
...
- ) : detail ? (
+ ) : isDetailError ? (
+ <p className="font-designer-14r text-text-subtle">
+ 문의 내용을 불러오지 못했어요. 잠시 후 다시 시도해 주세요.
+ </p>
+ ) : detail ? (As per coding guidelines: "All errors must be handled via utils/error-handler.ts — never use alert(), always use useToastStore."
Also applies to: 139-173
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/`(service)/(my)/my-inquiry/page.tsx around lines 95 - 96, The detail
query call using useGetMyOneToOneInquiryDetail (with expanded ?
inquiry.oneToOneInquiryId : null) currently only handles loading/success and
leaves the panel empty on failure; update the component to read the hook's error
state (e.g., isError and error) alongside detail and detailLoading, call the
centralized error handler from utils/error-handler.ts (and/or useToastStore) to
report the error, and render an explicit error/fallback UI in the panel (a brief
error message or retry button) when isError is true so the user doesn’t see an
empty panel; apply the same pattern to the related rendering block around the
139-173 area.
| ? 'border-b-2 border-rose-500 text-rose-500' | ||
| : 'font-designer-20r text-gray-800', |
There was a problem hiding this comment.
하드코딩된 색상 클래스는 토큰 클래스로 교체해 주세요.
border-rose-500, text-rose-500, border-rose-300, hover:text-red-500는 디자인 토큰 규칙과 충돌합니다. global.css의 토큰 기반 색상 클래스로 통일하는 게 맞습니다.
As per coding guidelines src/app/**/*.{tsx,jsx}: Never use Tailwind base classes ... Always use project custom tokens from global.css.
Also applies to: 95-95, 235-235
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/`(service)/(my)/my-posts/page.tsx around lines 78 - 79, Replace
hardcoded Tailwind color classes in the conditional className (the ternary that
returns 'border-b-2 border-rose-500 text-rose-500' vs 'font-designer-20r
text-gray-800') and the other occurrences mentioned (the lines using
'border-rose-300' and 'hover:text-red-500') with the project’s design token
classes defined in global.css; locate the conditional that sets those class
strings in src/app/(service)/(my)/my-posts/page.tsx and swap each color class
(border-rose-500, text-rose-500, border-rose-300, hover:text-red-500,
text-gray-800 as appropriate) for the equivalent token-based classes (the token
names used elsewhere in global.css) so styling follows the token convention.
| href={`/class/${feed.courseId}/lesson/${feed.lessonId}?feedId=${feed.feedId}`} | ||
| className="flex flex-col" | ||
| > | ||
| <div className="relative aspect-[3/2] w-full bg-gray-100"> |
There was a problem hiding this comment.
aspect-[3/2]는 금지된 arbitrary value입니다.
현재 경로(src/app) 규칙상 arbitrary 값을 사용할 수 없습니다. 프로젝트에 정의된 비율 토큰/유틸 클래스로 치환해 주세요.
As per coding guidelines src/**/*.{ts,tsx,css}: No Tailwind arbitrary values ... and src/app/**/*.{tsx,jsx}: Do not use arbitrary Tailwind values in className attributes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/`(service)/(my)/my-posts/page.tsx at line 248, Replace the forbidden
Tailwind arbitrary class "aspect-[3/2]" on the <div className="relative
aspect-[3/2] w-full bg-gray-100"> JSX element in page.tsx with the project’s
predefined aspect-ratio token/utility (e.g., the project's aspect token such as
aspect-<token> or ratio-<token>); locate that JSX node (in the default export
component of page.tsx) and swap the arbitrary value for the correct
token/utility class used across the codebase.
| inquiryContent: z | ||
| .string() | ||
| .min(1, '내용을 입력해 주세요.') | ||
| .max(2000, '내용은 2000자 이하로 입력해 주세요.'), |
There was a problem hiding this comment.
공백만 있는 내용이 스키마를 통과합니다.
.min(1)만 쓰면 공백 문자열도 유효값이 됩니다. 지금 작성 페이지는 trim()으로 막고 있어서 검증 규칙이 이미 이 스키마와 어긋나 있습니다.
🔧 제안
inquiryContent: z
.string()
+ .trim()
.min(1, '내용을 입력해 주세요.')
.max(2000, '내용은 2000자 이하로 입력해 주세요.'),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/types/schemas/inquiry.schema.ts` around lines 26 - 29, The inquiryContent
string schema currently uses .min(1) which allows strings containing only
whitespace; update the schema for inquiryContent to reject whitespace-only input
by trimming or validating whitespace before length check—for example, apply
.transform(s => s.trim()) then .min(1, '내용을 입력해 주세요.') or use .refine(s =>
s.trim().length > 0, { message: '내용을 입력해 주세요.' }) so the schema behavior matches
the form's trim logic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/app/(service)/(my)/my-posts/page.tsx (1)
410-417:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
next/image원격 URL(feed.thumbnailUrl)은 렌더 전 remotePatterns 허용 호스트 검증이 필요합니다
src/app/(service)/(my)/my-posts/page.tsx에서feed.thumbnailUrl && !imgError조건만으로<Image src={feed.thumbnailUrl} ... />를 렌더하고(410-417, 510-517),next.config.tsimages.remotePatterns에 매칭되지 않는 URL이면 렌더 단계에서 예외가 날 수 있습니다.remotePatterns기준(예:img1.kakaocdn.net,lh3.googleusercontent.com,test-api.zeroone.it.kr,api.zeroone.it.kr,www.zeroone.it.kr,**.r2.cloudflarestorage.com, 프로덕션 외 localhost 등)으로 URL의 hostname(및 필요 시 protocol/path)을 사전 가드한 뒤 허용되는 경우에만<Image>를 렌더하세요(허용 불가 시 fallback div로 처리).- (동일 구간)
className="relative aspect-[3/2] w-full bg-gray-100"처럼 arbitrary 값/하드코딩 컬러가 섞여 있어global.css토큰으로 치환이 필요합니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`(service)/(my)/my-posts/page.tsx around lines 410 - 417, The code renders next/image with feed.thumbnailUrl when feed.thumbnailUrl && !imgError which can throw at render if the remote host isn't allowed by next.config.images.remotePatterns; before rendering the <Image> in the component (the block using feed.thumbnailUrl, imgError, Image, and setImgError) validate the URL's hostname (and protocol/path if needed) against the canonical allowed-host list used in next.config (e.g., img1.kakaocdn.net, lh3.googleusercontent.com, test-api.zeroone.it.kr, api.zeroone.it.kr, www.zeroone.it.kr, *.r2.cloudflarestorage.com, localhost variants) and only render <Image src={feed.thumbnailUrl} ... /> when it matches, otherwise render the existing fallback div; also replace the hardcoded tailwind/bg class "relative aspect-[3/2] w-full bg-gray-100" with a class or token referenced from global.css (use the global CSS variable/token for the background color) so styling uses the design tokens instead of arbitrary values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/payment/modals/class-refund-request-modal.tsx`:
- Around line 132-136: The onChange currently force-casts the dropdown value
with a bare assertion (onChange={(v) => setReason(v as
CourseRefundReasonCode)}); remove the bare as and perform a runtime guard
against valid REFUND_REASONS before calling setReason. Implement a small type
guard that checks whether the incoming v matches one of the known REFUND_REASONS
values (or has the expected discriminant/shape), then call setReason with the
narrowed value; if the guard fails, set a safe fallback (e.g., undefined or a
default reason). Update the SingleDropdown onChange to use this guard and
fallback instead of the bare cast, referencing SingleDropdown, REFUND_REASONS,
reason, setReason, and CourseRefundReasonCode.
In `@src/hooks/queries/course/course-api.ts`:
- Around line 974-987: The current useGetMyDraftBuilderFeeds hook always returns
an empty list because enabled: false and a hardcoded queryFn; fix by wiring it
to an actual readiness flag and/or real API: replace enabled: false with a
conditional flag (e.g., isBuilderFeedsAvailable or isBackendReady) and update
queryFn inside useGetMyDraftBuilderFeeds to call
axiosInstanceV5.get('members/me/builder-feeds/draft') and return data.content
when that flag is true, otherwise keep returning the placeholder ({ feeds: [],
totalCount: 0 }) but ensure the UI checks the same flag (or a returned status)
to show “준비 중” or hide the feature instead of silently showing an empty list;
reference useGetMyDraftBuilderFeeds, queryFn, queryKey and enabled when making
the change.
- Around line 310-329: The mutation useRequestCourseRefund currently only
handles success and leaves failures unhandled; add an onError handler to the
useMutation options that routes the caught error through the project's error
handler and toast store: call the shared error handler from
utils/error-handler.ts (or its exported function) and then use useToastStore to
show a user-facing message when mutationFn for paymentId /
CourseRefundCreateRequest fails, keeping existing onSuccess
(queryClient.invalidateQueries(['myCoursePayments'])) intact; reference the
useRequestCourseRefund function, mutationFn, onError, useToastStore, and
utils/error-handler.ts when making the change.
---
Outside diff comments:
In `@src/app/`(service)/(my)/my-posts/page.tsx:
- Around line 410-417: The code renders next/image with feed.thumbnailUrl when
feed.thumbnailUrl && !imgError which can throw at render if the remote host
isn't allowed by next.config.images.remotePatterns; before rendering the <Image>
in the component (the block using feed.thumbnailUrl, imgError, Image, and
setImgError) validate the URL's hostname (and protocol/path if needed) against
the canonical allowed-host list used in next.config (e.g., img1.kakaocdn.net,
lh3.googleusercontent.com, test-api.zeroone.it.kr, api.zeroone.it.kr,
www.zeroone.it.kr, *.r2.cloudflarestorage.com, localhost variants) and only
render <Image src={feed.thumbnailUrl} ... /> when it matches, otherwise render
the existing fallback div; also replace the hardcoded tailwind/bg class
"relative aspect-[3/2] w-full bg-gray-100" with a class or token referenced from
global.css (use the global CSS variable/token for the background color) so
styling uses the design tokens instead of arbitrary values.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 414a6ef6-1797-4881-91ba-1fd95bfe3a1f
📒 Files selected for processing (6)
src/app/(service)/(my)/class-payment-management/page.tsxsrc/app/(service)/(my)/my-posts/page.tsxsrc/components/payment/modals/class-cancel-payment-modal.tsxsrc/components/payment/modals/class-refund-request-modal.tsxsrc/hooks/queries/course/course-api.tssrc/types/api/course.types.ts
| <SingleDropdown | ||
| options={REFUND_REASONS} | ||
| value={reason} | ||
| onChange={(v) => setReason(v as CourseRefundReasonCode)} | ||
| placeholder="사유를 선택해 주세요." |
There was a problem hiding this comment.
드롭다운 값의 bare as 단언은 제거해 주세요.
onChange={(v) => setReason(v as CourseRefundReasonCode)}는 런타임 검증 없이 union 타입을 강제합니다. 가이드대로 in 가드 + fallback으로 좁혀서 상태를 설정해야 안전합니다.
As per coding guidelines: src/components/**/*.{ts,tsx}에서 “Use the in guard operator with fallback ... Never use bare as assertions without runtime guards”.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/payment/modals/class-refund-request-modal.tsx` around lines
132 - 136, The onChange currently force-casts the dropdown value with a bare
assertion (onChange={(v) => setReason(v as CourseRefundReasonCode)}); remove the
bare as and perform a runtime guard against valid REFUND_REASONS before calling
setReason. Implement a small type guard that checks whether the incoming v
matches one of the known REFUND_REASONS values (or has the expected
discriminant/shape), then call setReason with the narrowed value; if the guard
fails, set a safe fallback (e.g., undefined or a default reason). Update the
SingleDropdown onChange to use this guard and fallback instead of the bare cast,
referencing SingleDropdown, REFUND_REASONS, reason, setReason, and
CourseRefundReasonCode.
| export const useRequestCourseRefund = () => { | ||
| const queryClient = useQueryClient(); | ||
|
|
||
| return useMutation({ | ||
| mutationFn: async ({ | ||
| paymentId, | ||
| request, | ||
| }: { | ||
| paymentId: number; | ||
| request: CourseRefundCreateRequest; | ||
| }) => { | ||
| await axiosInstanceV5.post( | ||
| `course-payments/${paymentId}/refunds`, | ||
| request, | ||
| ); | ||
| }, | ||
| onSuccess: async () => { | ||
| await queryClient.invalidateQueries({ queryKey: ['myCoursePayments'] }); | ||
| }, | ||
| }); |
There was a problem hiding this comment.
환불 요청 실패 시 사용자 피드백 경로가 비어 있습니다.
useRequestCourseRefund는 성공 시 무효화만 처리하고 실패 시 토스트/에러 처리(onError)가 없어 UX가 끊깁니다. 실패 케이스도 useToastStore(또는 공통 error-handler 경유)로 명시 처리해 주세요.
As per coding guidelines: src/**/*.{ts,tsx}에서 “All errors must be handled via utils/error-handler.ts ... use useToastStore”.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/queries/course/course-api.ts` around lines 310 - 329, The mutation
useRequestCourseRefund currently only handles success and leaves failures
unhandled; add an onError handler to the useMutation options that routes the
caught error through the project's error handler and toast store: call the
shared error handler from utils/error-handler.ts (or its exported function) and
then use useToastStore to show a user-facing message when mutationFn for
paymentId / CourseRefundCreateRequest fails, keeping existing onSuccess
(queryClient.invalidateQueries(['myCoursePayments'])) intact; reference the
useRequestCourseRefund function, mutationFn, onError, useToastStore, and
utils/error-handler.ts when making the change.
| export const useGetMyDraftBuilderFeeds = () => { | ||
| return useQuery({ | ||
| queryKey: ['myDraftBuilderFeeds'], | ||
| queryFn: async (): Promise<{ | ||
| feeds: MyDraftBuilderFeedItemResponse[]; | ||
| totalCount: number; | ||
| }> => { | ||
| // TODO: replace with real API call when backend is ready | ||
| // const { data } = await axiosInstanceV5.get('members/me/builder-feeds/draft'); | ||
| // return data.content; | ||
| return { feeds: [], totalCount: 0 }; | ||
| }, | ||
| enabled: false, | ||
| }); |
There was a problem hiding this comment.
임시 저장 피드 쿼리가 영구 비활성이라 기능이 항상 빈 목록으로 보입니다.
현재 enabled: false + 고정 빈 배열 반환으로 실제 데이터 조회가 절대 일어나지 않습니다. 백엔드 미오픈 상태라면 UI에서 “준비 중”을 명시적으로 표시하거나, 기능 노출 자체를 조건부로 막는 편이 안전합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/queries/course/course-api.ts` around lines 974 - 987, The current
useGetMyDraftBuilderFeeds hook always returns an empty list because enabled:
false and a hardcoded queryFn; fix by wiring it to an actual readiness flag
and/or real API: replace enabled: false with a conditional flag (e.g.,
isBuilderFeedsAvailable or isBackendReady) and update queryFn inside
useGetMyDraftBuilderFeeds to call
axiosInstanceV5.get('members/me/builder-feeds/draft') and return data.content
when that flag is true, otherwise keep returning the placeholder ({ feeds: [],
totalCount: 0 }) but ensure the UI checks the same flag (or a returned status)
to show “준비 중” or hide the feature instead of silently showing an empty list;
reference useGetMyDraftBuilderFeeds, queryFn, queryKey and enabled when making
the change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
useGetMyBuilderFeedManagement({ status: 'DRAFT' })로 통합
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- feed/write: draftFeedIdRef로 feedId 추적, 재저장 시 updateFeed 사용
- feed/write: 등록하기 시 기존 draft는 updateFeed(PUBLISHED)로 전환
- feed/write: edit mode 수정하기에 status: PUBLISHED 추가
- qa/write: 등록 성공 시 localStorage draft 정리 추가
- my-posts: useGetMyBuilderFeedManagement({ status: DRAFT })로 교체
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/`(service)/(my)/my-posts/page.tsx:
- Around line 457-461: The editHref currently builds slug routes using
feed.courseId which can break slug-based routing; update the logic around
editHref (used where feed.courseId, feed.lessonId, feed.feedId are referenced)
to prefer feed.courseSlug when present and fall back safely (e.g., render no
link or a disabled/placeholder link) while the slug is not available, or ensure
the API returns courseSlug and use that to construct `/class/${courseSlug}` and
`/class/${courseSlug}/lesson/${feed.lessonId}?feedId=${feed.feedId}`; make sure
link rendering checks feed.courseSlug before outputting a slug route to avoid
broken links.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 6621b314-ca1d-4d0e-a914-da134f67cbe0
📒 Files selected for processing (6)
src/app/(class-lesson)/class/[slug]/lesson/[id]/_components/lesson-qna-submission-modal.tsxsrc/app/(landing)/class/[slug]/(learning)/feed/write/page.tsxsrc/app/(landing)/class/[slug]/(learning)/qa/write/page.tsxsrc/app/(service)/(my)/my-posts/page.tsxsrc/hooks/queries/course/course-api.tssrc/types/api/course.types.ts
💤 Files with no reviewable changes (1)
- src/hooks/queries/course/course-api.ts
✅ Files skipped from review due to trivial changes (1)
- src/app/(class-lesson)/class/[slug]/lesson/[id]/_components/lesson-qna-submission-modal.tsx
| // TODO: courseSlug not in API response — using courseId as slug placeholder | ||
| const editHref = | ||
| feed.lessonId !== null | ||
| ? `/class/${feed.courseId}/lesson/${feed.lessonId}?feedId=${feed.feedId}` | ||
| : `/class/${feed.courseId}`; |
There was a problem hiding this comment.
[slug] 라우트에 courseId를 대입하면 수정 링크가 깨질 수 있습니다.
현재 editHref가 "/class/${feed.courseId}"를 사용하고 있어, slug 기반 라우팅과 불일치할 가능성이 큽니다. courseSlug를 응답에 포함해 경로를 구성하거나, slug가 준비될 때까지 해당 분기 링크를 비활성/대체 처리해 주세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/`(service)/(my)/my-posts/page.tsx around lines 457 - 461, The
editHref currently builds slug routes using feed.courseId which can break
slug-based routing; update the logic around editHref (used where feed.courseId,
feed.lessonId, feed.feedId are referenced) to prefer feed.courseSlug when
present and fall back safely (e.g., render no link or a disabled/placeholder
link) while the slug is not available, or ensure the API returns courseSlug and
use that to construct `/class/${courseSlug}` and
`/class/${courseSlug}/lesson/${feed.lessonId}?feedId=${feed.feedId}`; make sure
link rendering checks feed.courseSlug before outputting a slug route to avoid
broken links.
임시저장 글 등록 시 피드 목록이 아닌 발행된 피드 상세로 이동. 신규 등록도 createFeed 응답의 feedId로 상세 페이지 이동. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
test.zeroone.it.kr DNS 불통 상태에서 전체 E2E 테스트가 net::ERR_NAME_NOT_RESOLVED로 실패하는 문제 해결. 커넥티비티 프리플라이트 추가 — 응답 없으면 STAGING_DOWN=true 설정 후 E2E 스텝 전체 스킵, 경고 메시지 출력. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
이전 bandaid fix(스테이징 다운 시 전체 스킵)를 대체. - CI에서 Next.js 앱 직접 빌드 후 localhost:3000 서버 기동 - non-@auth 테스트: 항상 로컬 서버 대상 실행 (스테이징 불필요) - @auth 테스트: 스테이징 가용 시에만 실행, 불가 시 경고 후 스킵 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Problem
마이페이지 관련 기능이 미구현 상태이거나 디자인 토큰 불일치 및 UI 완성도 부족 문제가 있었습니다.
primary-500등 미존재 토큰 사용Solution
localStorage기반 클라이언트 상태 관리로 즉시 반영ToggleSwitch로 교체font-designer-18r/24b), 색상(text-text-default), 간격 토큰 Figma 기준으로 수정primary-500→text-text-brand,bg-fill-brand-default-default등 시스템 토큰으로 전환Changes
Features
src/app/(service)/(my)/my-posts/page.tsxsrc/app/(service)/(my)/my-inquiry/page.tsxsrc/app/(service)/(my)/my-inquiry/write/page.tsxsrc/hooks/queries/my-inquiry/inquiry-api.tssrc/types/schemas/inquiry.schema.tssrc/api/client/axios.tsaxiosInstanceV6추가src/app/(service)/(my)/my-class/_components/disable-notification-modal.tsxBug Fixes
src/app/(service)/(my)/my-page/page.tsxsrc/app/(service)/(my)/my-class/page.tsxsrc/app/(service)/(my)/my-page/_components/withdrawal-confirm-modal.tsxResult
Screenshots
Test plan
yarn typecheck통과 확인🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Style
Chores